# AlbumArtDownloader - Search and download album art from the internet.
#
# Copyright (C) 2007 Lauri Taimila
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

__licence__ = "GPLv2"
__copyright__ = "2007, Lauri Taimila"
__author__ = "Lauri Taimila <lauri@taimila.com>"

import os
import re
import locale
import urllib
import threading
from xml.dom import minidom

class Bag: pass # Just a helper

class AlbumArtDownloader(threading.Thread):
    """
    Search and download album art from the internet.
    
    This class is heavily based on Rhythmbox - AlbumArt plugin's class
    'AmazonCoverArtSearch'. That plugin is released under GPLv2 (or higher)
    and it's copyrights belong to Gareth Murphy and Martin Szulecki.
 
    See more: http://www.gnome.org/projects/rhythmbox/
    
    If you want better cover search, please contribute to Rhyhtmbox project.
    """

    # Amazon licence for Entertainer
    LICENSE_KEY = "1YCWYZ0SPPAJ27YZZ482" #FIXME: doesn't work?!
    
    # Default locale for Amazon search
    DEFAULT_LOCALE = "en_US"

    # Supported locales
    AMAZON_LOCALES = {"en_US" : ("us", "xml.amazon.com"),
            		  "en_GB" : ("uk", "xml-eu.amazon.com"),
            	      "de" : ("de", "xml-eu.amazon.com"),
            		  "ja" : ("jp", "xml.amazon.co.jp") }
    
    ASSOCIATE = "webservices-20"

    def __init__(self, album, artist, art_file_path, callback = None):
        """
        Initialize album art downloader
        @param album: Album title
        @param artist: Artist name
        @param art_file_path: Path where albumart is saved
        @param callback: Callback function that is called after search if set
        """
        threading.Thread.__init__(self)
        self.setName("AlbumArt Downloader")
        self.callback_function = callback        # Callback function
        self.album = album                       # Album title
        self.artist = artist                     # Artist name
        self.path = art_file_path                # Album art files are in this directory
        
    def run(self):
        """Start searching and downloading albumart."""
        self.__search()

    def __getLocale(self):
        """Get locale for URL."""
        default = locale.getdefaultlocale()
        lc_id = self.DEFAULT_LOCALE
        if default[0] is not None:
            if self.AMAZON_LOCALES.has_key(default[0]):
                lc_id = default[0]

        lc_host = self.AMAZON_LOCALES[lc_id][1]
        lc_name = self.AMAZON_LOCALES[lc_id][0]
        return ((lc_host, lc_name))

    def __buildURL(self, keyword):
        """ Create a search URL for album. """
        (lc_host, lc_name) = self.__getLocale()
        url = "http://" + lc_host + "/onca/xml3?f=xml"
        url += "&t=%s" % self.ASSOCIATE
        url += "&dev-t=%s" % self.LICENSE_KEY
        url += "&type=%s" % 'lite'
        url += "&locale=%s" % lc_name
        url += "&mode=%s" % 'music'
        url += "&%s=%s" % ('KeywordSearch', urllib.quote(keyword.encode("utf8")))
        return url

    def __tidyUpString(self, s):
        """Tidy up string. Remove spaces, convert to lowercase and replace chars."""
        s = s.lower()
        s = s.strip()
        s = s.replace(" - ", " ")	
        s = s.replace(": ", " ")
        s = s.replace(" & ", " and ")
        return s
		
    def __search(self):
        """Search album art from Amazon"""
        self.searching = True
        self.cancel = False
        self.keywords = []

        st_artist = self.artist
        st_album = self.album

        # Replace quote characters
        # don't replace single quote: could be important punctuation
        for char in ["\""]:
        	st_artist = st_artist.replace(char, '')
        	st_album = st_album.replace(char, '')

        self.st_album = st_album
        self.st_artist = st_artist

        # Remove variants of Disc/CD [1-9] from album title before search
        for exp in ["\([Dd]isc *[1-9]+\)", "\([Cc][Dd] *[1-9]+\)"]:
        	p = re.compile (exp)
        	st_album = p.sub ('', st_album)

        st_album_no_vol = st_album
        for exp in ["\(*[Vv]ol.*[1-9]+\)*"]:
        	p = re.compile (exp)
        	st_album_no_vol = p.sub ('', st_album_no_vol)

        self.st_album_no_vol = st_album_no_vol

        # Save current search's entry properties
        self.search_album = st_album
        self.search_artist = st_artist
        self.search_album_no_vol = st_album_no_vol

        # Assemble list of search keywords (and thus search queries)
        if st_album == ("Unknown"):
        	self.keywords.append ("%s Best of" % (st_artist))
        	self.keywords.append ("%s Greatest Hits" % (st_artist))
        	self.keywords.append ("%s Essential" % (st_artist))
        	self.keywords.append ("%s Collection" % (st_artist))
        	self.keywords.append ("%s" % (st_artist))
        elif st_artist == ("Unknown"):
        	self.keywords.append ("%s" % (st_album))
        	if st_album_no_vol != st_artist:
        		self.keywords.append ("%s" % (st_album_no_vol))
        	self.keywords.append ("Various %s" % (st_album))
        else:
        	if st_album != st_artist:
        		self.keywords.append ("%s %s" % (st_artist, st_album))
        		if st_album_no_vol != st_album:
        			self.keywords.append ("%s %s" % (st_artist, st_album_no_vol))
        		if (st_album != ("Unknown")):
        			self.keywords.append ("Various %s" % (st_album))
        	self.keywords.append ("%s" % (st_artist))

        # Initiate asynchronous search
        self.__searchNext();

    def __searchNext(self):
        """Search again, because the last one didn't find any covers."""
        self.searching = True

        if len(self.keywords) == 0:
        	keyword = None
        else:
        	keyword = self.keywords.pop(0)

        if keyword is None:
        	# No keywords left to search -> no results
        	self.__searchCompleted(None)
        	return False
        else:
            # Retrieve search for keyword
            url = self.__buildURL(keyword.strip())
            temp = urllib.urlopen(url)
            search_results = temp.read()
            self.__checkSearchResults(search_results)
            return True

    def __unmarshal(self, element):
		rc = Bag()
		if isinstance(element, minidom.Element) and (element.tagName == 'Details'):
			rc.URL = element.attributes["url"].value
		childElements = [e for e in element.childNodes if isinstance(e, minidom.Element)]
		if childElements:
			for child in childElements:
				key = child.tagName
				if hasattr(rc, key):
					if type (getattr(rc, key)) <> type([]):
						setattr(rc, key, [getattr(rc, key)])
					setattr(rc, key, getattr(rc, key) + [self.__unmarshal (child)])
				elif isinstance(child, minidom.Element) and (child.tagName == 'Details'):
					setattr(rc,key,[self.__unmarshal(child)])
				else:
					setattr(rc, key, self.__unmarshal(child))
		else:
			rc = "".join ([e.data for e in element.childNodes if isinstance (e, minidom.Text)])
			if element.tagName == 'SalesRank':
				rc = rc.replace ('.', '')
				rc = rc.replace (',', '')
				rc = int(rc)
		return rc

    def __checkSearchResults(self, result_data):
        """
        Check search results.

        If results are not good, we search again with the next keyword.
        """
        if result_data is None:
        	self.__searchNext()
        	return
        try:
            xmldoc = minidom.parseString (result_data)
        except:
        	self.__searchNext()
        	return

        data = self.__unmarshal(xmldoc).ProductInfo

        if hasattr(data, 'ErrorMsg'):
        	# Search was unsuccessful, try next keyword
        	self.__searchNext()
        else:
        	# We got some search results and that's enough for us
        	self.__searchCompleted(data.Details)

    def __searchCompleted(self, result):
        """
        Search completed and results found.
        
        Download large album art image from the first result and save it to
        the disk.
        """
        self.searching = False
        image_urls = self.__getBestMatchImageURLs(result)
        if len(image_urls) == 0:
            return
        image_url = image_urls[0]
        #image_url = result[0].ImageUrlLarge # This doesn't check which result is the best match
        try:
            image_file = urllib.urlopen(image_url)
        except:
            return
        dest = open(os.path.join(self.path, self.artist + " - " + self.album + ".jpg"),'w')
        dest.write(image_file.read())
        dest.close()
        
        if self.callback_function is not None:
            self.callback_function(self.artist, self.album)
		
    def __getBestMatchImageURLs(self, search_results):
        """Return tuple of URL's to large and medium cover of the best match"""
        # Default to "no match", our results must match our criteria
        best_match = None

        try:
        	if self.search_album != ("Unknown"):
        		album_check = self.__tidyUpString (self.search_album)
        		for item in search_results:
        			# Check for album name in ProductName
        			product_name = self.__tidyUpString (item.ProductName)

        			if product_name == album_check:
        				# Found exact album, can not get better than that
        				best_match = item
        				break
        			# If we already found a best_match, just keep checking for exact one
        			elif (best_match is None) and (product_name.find (album_check) != -1):
        				best_match = item

        	# If we still have no definite hit, use first result where artist matches
        	if (self.search_album == ("Unknown") and self.search_artist != ("Unknown")):
        		artist_check = self.__tidyUpString (self.search_artist)
        		if best_match is None:
        			# Check if artist appears in the Artists list
        			hit = False
        			for item in search_results:
        				if type (item.Artists.Artist) <> type ([]):
        					artists = [item.Artists.Artist]
        				else:
        					artists = item.Artists.Artist

        				for artist in artists:
        					artist = self.__tidyUpString (artist)
        					if artist.find (artist_check) != -1:
        						best_match = item
        						hit = True						
        						break
        				if hit:
        					break

        	if best_match: 
        		return (best_match.ImageUrlLarge, best_match.ImageUrlMedium)
        	else:
        		return ()

        except TypeError:
        	return ()
        	
